package junior.util.mapstruct;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import junior.util.compiler.JavaStringCompiler;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.ap.MappingProcessor;
import org.mapstruct.factory.Mappers;

/**
 * Name: MapStructGenerator
 * Function:
 *
 * @Author: K.K
 * Create Time: 2020/12/08 14:36
 * Modified By:
 * Modified Time:
 * Description:
 * Version:
 */
public class MapStructGenerator {

    private final JavaStringCompiler compiler;
    private String output;
    private final String namespace;
    private final Map<String, SimpleMapStruct> localeMapStruct;
    // 核心依赖
    private final Set<String> dependentSet;

    public MapStructGenerator() {
        this(false, null, null);
    }

    public MapStructGenerator(Boolean isShow) {
        this(isShow, null, null);
    }

    public MapStructGenerator(Boolean isShow, String namespace, Map<String, SimpleMapStruct> mappers) {
        //
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        //
        this.compiler = new JavaStringCompiler();
        if (BooleanUtils.isTrue(isShow)) {
            this.output = Objects.requireNonNull(classLoader.getResource("")).getPath();
        }
        this.namespace = Optional.ofNullable(namespace).orElse("mapstruct");
        // 防止并发
        this.localeMapStruct = Maps.newConcurrentMap();
        if (MapUtils.isNotEmpty(mappers)) {
            this.localeMapStruct.putAll(mappers);
        }
        //
        this.dependentSet = Sets.newHashSet();
        this.dependentSet.add(getJarPath(MapStructGenerator.class));
        this.dependentSet.add(getJarPath(Mappers.class));
        this.dependentSet.add(getJarPath(MappingProcessor.class));
        for (URL url : ((URLClassLoader) classLoader).getURLs()) {
            this.dependentSet.add(url.getPath());
        }
    }

    private <T extends SimpleMapStruct> T getMapper(Class<?> sourceClass, Class<?> targetClass) {
        return (T) localeMapStruct.get(getMapStructKey(sourceClass, targetClass));
    }

    private <T extends SimpleMapStruct> T getMapper(Class<?> beanClass, boolean inverse) {
        String name = beanClass.getSimpleName();
        return (T) localeMapStruct.entrySet().stream().filter(entry -> inverse ? entry.getKey().endsWith(name) : entry.getKey().startsWith(name)).findFirst().map(Map.Entry::getValue).orElse(null);
    }

    /**
     * 静态注册
     *
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @param mapperClass 继承BaseMapStruct的类型
     */
    public void registration(Class<?> sourceClass, Class<?> targetClass, Class<? extends SimpleMapStruct> mapperClass) {
        localeMapStruct.computeIfAbsent(getMapStructKey(sourceClass, targetClass), key -> Mappers.getMapper(mapperClass));
    }

    /**
     * 动态注册
     *
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @param keyMappings 属性映射键值对：sourceProperty1,targetProperty1,sourceProperty2,targetProperty2...
     * @return
     */
    public <T extends SimpleMapStruct> T registration(Class<?> sourceClass, Class<?> targetClass, String... keyMappings) {
        return (T) localeMapStruct.computeIfAbsent(getMapStructKey(sourceClass, targetClass), (key) -> {
            //
            Map<String, String> toMap = null;
            if (null != keyMappings && keyMappings.length > 0) {
                if (keyMappings.length % 2 != 0) {
                    throw new RuntimeException("属性映射键值对不匹配");
                }

                toMap = Maps.newHashMap();
                for (int i = 0; i < keyMappings.length; ) {
                    toMap.put(keyMappings[i], keyMappings[i + 1]);
                    i += 2;
                }
            }
            try {
                String code = generateJavaSource(namespace, sourceClass, targetClass, toMap);
                String name = getName(namespace, sourceClass, targetClass);
                List<String> options = Lists.newArrayList("-classpath", getDependentJars(sourceClass, targetClass));
                if (StringUtils.isNotBlank(output)) {
                    options.add("-d");
                    options.add(output);
                }
                Map<String, byte[]> map = compiler.compile(name, code, options);
                return (T) Mappers.getMapper(compiler.loadClass(name, map));
            } catch (Exception e) {
                throw new RuntimeException("MapStruct动态编译异常: " + e.getMessage());
            }
        });
    }

    public <T> T map(Object source, Class<T> targetClass) {
        if (null == source) {
            return null;
        }
        Class<?> sourceClass = source.getClass();
        SimpleMapStruct mapper = getMapper(sourceClass, targetClass);
        if (null != mapper) {
            return (T) mapper.map(source);
        }
        mapper = getMapper(targetClass, sourceClass);
        if (null != mapper && mapper instanceof BaseMapStruct) {
            return (T) ((BaseMapStruct) mapper).inverseMap(source);
        }
        // 动态编译
        mapper = registration(sourceClass, targetClass);
        if (null != mapper) {
            return (T) mapper.map(source);
        }
        return null;
    }

    public Object copy(Object source, Object target) {
        if (null == source || null == target) {
            return null;
        }
        Class<?> sourceClass = source.getClass();
        Class<?> targetClass = target.getClass();
        if (isSimplified(sourceClass, targetClass)) {
            SimpleMapStruct mapper = getMapper(sourceClass, false);
            if (null != mapper) {
                if (mapper instanceof BaseMapStruct) {
                    ((BaseMapStruct) mapper).mirrorCopy(source, target);
                } else {
                    mapper.copy(source, target);
                }
                return target;
            }
            mapper = getMapper(sourceClass, true);
            if (null != mapper) {
                if (mapper instanceof BaseMapStruct) {
                    ((BaseMapStruct) mapper).inverseMirrorCopy(source, target);
                } else {
                    mapper.copy(source, target);
                }
                return target;
            }
            mapper = registration(sourceClass, sourceClass);
            if (null != mapper) {
                mapper.copy(source, target);
                return target;
            }
        } else {
            BaseMapStruct mapper = getMapper(sourceClass, targetClass);
            if (null != mapper) {
                mapper.copy(source, target);
                return target;
            }
            mapper = getMapper(targetClass, sourceClass);
            if (null != mapper) {
                mapper.inverseCopy(source, target);
                return target;
            }
            mapper = registration(sourceClass, targetClass);
            if (null != mapper) {
                mapper.copy(source, target);
                return target;
            }
        }
        return null;
    }

    public <T> List<T> mapList(Collection sources, Class<T> targetClass) {
        if (null == sources || sources.isEmpty()) {
            return (List<T>) sources;
        }
        Class<?> sourceClass = sources.stream().findFirst().get().getClass();
        SimpleMapStruct mapper = getMapper(sourceClass, targetClass);
        if (null != mapper) {
            return mapper.mapList(sources);
        }
        mapper = getMapper(targetClass, sourceClass);
        if (null != mapper && mapper instanceof BaseMapStruct) {
            return ((BaseMapStruct) mapper).inverseMapList(sources);
        }
        mapper = registration(sourceClass, targetClass);
        if (null != mapper) {
            return mapper.mapList(sources);
        }
        return null;
    }

    //region 动态生成MapStruct类

    /**
     * 生成Java源代码
     *
     * @param namespace   命名空间
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @param keyMappings 属性映射键值对
     * @return
     */
    public static String generateJavaSource(String namespace, Class<?> sourceClass, Class<?> targetClass, Map<String, String> keyMappings) {
        boolean isSimplified = isSimplified(sourceClass, targetClass);
        StringJoiner javaCode = new StringJoiner("\n");
        javaCode.add("package " + getPackage(namespace) + ";");
        javaCode.add("");
        javaCode.add("import " + sourceClass.getName() + ";");
        if (isSimplified) {
            javaCode.add("import junior.util.mapstruct.SimpleMapStruct;");
        } else {
            javaCode.add("import " + targetClass.getName() + ";");
            javaCode.add("import junior.util.mapstruct.BaseMapStruct;");
        }
        javaCode.add("import junior.util.mapstruct.TypeConversionStrategy;");
        javaCode.add("import org.mapstruct.Mapper;");
        javaCode.add("import org.mapstruct.Mapping;");
        javaCode.add("import org.mapstruct.Mappings;");
        javaCode.add("import org.mapstruct.NullValueCheckStrategy;");
        javaCode.add("import org.mapstruct.NullValuePropertyMappingStrategy;");
        javaCode.add("import org.mapstruct.ReportingPolicy;");
        javaCode.add("import org.mapstruct.control.DeepClone;");
        javaCode.add("");
        javaCode.add("@Mapper(uses = TypeConversionStrategy.class,");
        javaCode.add("        mappingControl = DeepClone.class,");
        javaCode.add("        unmappedTargetPolicy = ReportingPolicy.IGNORE,");
        javaCode.add("        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,");
        javaCode.add("        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)");
        javaCode.add("public interface " + getSimpleName(sourceClass, targetClass) + " extends " + (isSimplified ? "SimpleMapStruct" : "BaseMapStruct") + "<" + sourceClass.getSimpleName() + ", " + targetClass.getSimpleName() + "> {");
        if (MapUtils.isNotEmpty(keyMappings)) {
            javaCode.add("    @Mappings({");
            keyMappings.forEach((key, value) -> javaCode.add("            @Mapping(source = \"" + key + "\", target = \"" + value + "\"),"));
            javaCode.add("    })");
            javaCode.add("    @Override");
            javaCode.add("    " + targetClass.getSimpleName() + " map(" + sourceClass.getSimpleName() + " source);");
        }
        javaCode.add("}");
        return javaCode.toString();
    }

    /**
     * 是否简单模式
     *
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @return
     */
    private static boolean isSimplified(Class<?> sourceClass, Class<?> targetClass) {
        return sourceClass.equals(targetClass);
    }

    /**
     * Key
     *
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @return
     */
    private static String getMapStructKey(Class<?> sourceClass, Class<?> targetClass) {
        return isSimplified(sourceClass, targetClass) ? sourceClass.getSimpleName() : sourceClass.getSimpleName() + "2" + targetClass.getSimpleName();
    }

    /**
     * 包名
     *
     * @param namespace
     * @return
     */
    private static String getPackage(String namespace) {
        return "junior." + namespace + ".model";
    }

    /**
     * 类名
     *
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @return
     */
    private static String getSimpleName(Class<?> sourceClass, Class<?> targetClass) {
        return getMapStructKey(sourceClass, targetClass) + "MapStruct";
    }

    /**
     * 类全称
     *
     * @param namespace   命名空间
     * @param sourceClass 来源类型
     * @param targetClass 目标类型
     * @return
     */
    private static String getName(String namespace, Class<?> sourceClass, Class<?> targetClass) {
        return getPackage(namespace) + "." + getSimpleName(sourceClass, targetClass);
    }

    /**
     * 获取依赖类的 Jar 包路径
     *
     * @param classes
     * @return
     */
    private String getDependentJars(Class<?>... classes) {
        Set<String> dependencies = Sets.newHashSet(dependentSet.toArray(new String[]{}));
        if (null != classes && classes.length > 0) {
            for (Class<?> clazz : classes) {
                dependencies.add(getJarPath(clazz));
            }
        }
        return StringUtils.join(dependencies, File.pathSeparator);
    }

    /**
     * 获取 Jar 包路径
     *
     * @param clazz
     * @return
     */
    private String getJarPath(Class<?> clazz) {
        return clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
    }
    //endregion
}
